MySql锁与幻读、脏读详解

您所在的位置:网站首页 mysql 怎么念 MySql锁与幻读、脏读详解

MySql锁与幻读、脏读详解

#MySql锁与幻读、脏读详解| 来源: 网络整理| 查看: 265

一、mysql有关权限的表

MySQL服务器通过权限表来控制用户对数据库的访问,权限表存放在mysql数据库里,由mysql_install_db脚本初始化。这些权限表分别user,db,table_priv,columns_priv和host。下面分别介绍一下这些表的结构和内容:

user权限表:记录允许连接到服务器的用户帐号信息,里面的权限是全局级的。db权限表:记录各个帐号在各个数据库上的操作权限。table_priv权限表:记录数据表级的操作权限。columns_priv权限表:记录数据列级的操作权限。host权限表:配合db权限表对给定主机上数据库级操作权限作更细致的控制。这个权限表不受GRANT和REVOKE语句的影响。二、锁

根据加锁的范围,MySQL 里面的锁大致可以分成全局锁、表级锁和行锁三类。

1、全局锁:

顾名思义,全局锁就是对整个数据库实例加锁。MySQL 提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。

全局锁的典型使用场景是,做全库逻辑备份。//也就是把整库每个表都 select 出来存成文本。

官方自带的逻辑备份工具是mysqldump。当 mysqldump 使用参数–single-transaction 的时候,导数据之前就会在可重复读隔离级别下开启一个事务来确保拿到一致性视图。而由于 MVCC 的支持,这个过程中数据是可以正常更新的。所以,single-transaction 方法只适用于所有的表使用事务引擎的库。如果有的表使用了不支持事务的引擎,那么备份就只能通过 FTWRL 方法。这往往是 DBA 要求业务开发人员使用 InnoDB 替代 MyISAM 的原因之一。

既然要全库只读,为什么不使用 set global readonly=true 的方式呢?确实 readonly 方式也可以让全库进入只读状态,但我还是会建议你用 FTWRL 方式,主要有两个原因:一是,在有些系统中,readonly 的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库。因此,修改 global 变量的方式影响面更大,我不建议你使用。二是,在异常处理机制上有差异。如果执行 FTWRL 命令之后由于客户端发生异常断开,那么 MySQL 会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为 readonly 之后,如果客户端发生异常,则数据库就会一直保持 readonly 状态,这样会导致整个库长时间处于不可写状态,风险较高。

2、表级锁:

MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。表锁的语法是 lock tables … read/write。与 FTWRL 类似,可以用 unlock tables 主动释放锁,也可以在客户端断开的时候自动释放。需要注意,lock tables 语法除了会限制别的线程的读写外,也限定了本线程接下来的操作对象。

LOCK TABLES 和 UNLOCK TABLESMysql也支持lock tables和unlock tables,这都是在服务器层(MySQL Server层)实现的,和存储引擎无关,它们有自己的用途,并不能替代事务处理。 (除了禁用了autocommint后可以使用,其他情况不建议使用):

LOCK TABLES 可以锁定用于当前线程的表。如果表被其他线程锁定,则当前线程会等待,直到可以获取所有锁定为止。UNLOCK TABLES 可以释放当前线程获得的任何锁定。当前线程执行另一个 LOCK TABLES 时,或当与服务器的连接被关闭时,所有由当前线程锁定的表被隐含地解锁另一类表级的锁是 MDL(metadata lock)。MDL 不需要显式使用,在访问一个表的时候会被自动加上。在 MySQL 5.5 版本中引入了 MDL,当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁。读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。

共享锁(读锁):其他事务可以读,但不能写。排他锁(写锁) :其他事务不能读取,也不能写。

我们可以看到 session A 先启动,这时候会对表 t 加一个 MDL 读锁。由于 session B 需要的也是 MDL 读锁,因此可以正常执行。之后 session C 会被 blocked,是因为 session A 的 MDL 读锁还没有释放,而 session C 需要 MDL 写锁,因此只能被阻塞。如果只有 session C 自己被阻塞还没什么关系,但是之后所有要在表 t 上新申请 MDL 读锁的请求也会被 session C 阻塞。前面我们说了,所有对表的增删改查操作都需要先申请 MDL 读锁,就都被锁住,等于这个表现在完全不可读写了。如果某个表上的查询语句频繁,而且客户端有重试机制,也就是说超时后会再起一个新 session 再请求的话,这个库的线程很快就会爆满。你现在应该知道了,事务中的 MDL 锁,在语句执行开始时申请,但是语句结束后并不会马上释放,而会等到整个事务提交后再释放。

首先我们要解决长事务,事务不提交,就会一直占着 MDL 锁。在 MySQL 的 information_schema 库的 innodb_trx 表中,你可以查到当前执行中的事务。如果你要做 DDL 变更的表刚好有长事务在执行,要考虑先暂停 DDL,或者 kill 掉这个长事务。热点表kill 可能未必管用,因为新的请求马上就来了。比较理想的机制是,在 alter table 语句里面设定等待时间,如果在这个指定的等待时间里面能够拿到 MDL 写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。之后开发人员或者 DBA 再通过重试命令重复这个过程。MariaDB 已经合并了 AliSQL 的这个功能,所以这两个开源分支目前都支持 DDL NOWAIT/WAIT n 这个语法。

小结:全局锁主要用在逻辑备份过程中。对于全部是 InnoDB 引擎的库,我建议你选择使用–single-transaction 参数,对应用会更友好。表锁一般是在数据库引擎不支持行锁的时候才会被用到的。如果你发现你的应用程序里有 lock tables 这样的语句,你需要追查一下,比较可能的情况是:要么是你的系统现在还在用 MyISAM 这类不支持事务的引擎,那要安排升级换引擎;要么是你的引擎升级了,但是代码还没升级,把 lock tables 和 unlock tables 改成 begin 和 commit,问题就解决了。3、行锁:

在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。

另外还有间隙锁,间隙锁和行锁合称 next-key lock。下面继续介绍。4、死锁和死锁检测

当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁。

当出现死锁以后,有两种策略

一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置。另一种策略是,发起死锁检测(常用),发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。

在 InnoDB 中,innodb_lock_wait_timeout 的默认值是 50s,对于在线服务来说,这个等待时间往往是无法接受的。所以,正常情况下我们还是要采用第二种策略,即:主动死锁检测,而且 innodb_deadlock_detect 的默认值本身就是 on。主动死锁检测在发生死锁的时候,是能够快速发现并进行处理的,但是它也是有额外负担的。

怎么解决由这种热点行更新导致的性能问题呢?问题的症结在于,死锁检测要耗费大量的 CPU 资源。一种头痛医头的方法,就是如果你能确保这个业务一定不会出现死锁,可以临时把死锁检测关掉。但是这种操作本身带有一定的风险,可能会出现大量的超时,这是业务有损的。另一个思路是控制并发度。这个并发控制要做在数据库服务端。如果你有中间件,可以考虑在中间件实现;如果你的团队有能修改 MySQL 源码的人,也可以做在 MySQL 里面。基本思路就是,对于相同行的更新,在进入引擎之前排队。这样在 InnoDB 内部就不会有大量的死锁检测工作了。

如果出现死锁,可以用 SHOW INNODB STATUS 命令来确定最后一个死锁产生的原因。返回结果中包括死锁相关事务的详细信息,如引发死锁的 SQL 语句,事务已经获得的锁,正在等待什么锁,以及被回滚的事务等。据此可以分析死锁产生的原因和改进措施。

MyISAM避免死锁:在自动加锁的情况下,MyISAM 总是一次获得 SQL 语句所需要的全部锁,所以 MyISAM 表不会出现死锁。三、幻读、脏读1、幻读1)什么是幻读

简单理解就是事务A读到了事务B新插入的数据,导致数据不一致。

2)产生幻读的原因

行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的“间隙”。因此,为了解决幻读问题,InnoDB 只好引入新的锁,也就是间隙锁 (Gap Lock)。跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作。间隙锁之间都不存在冲突关系。

==间隙锁和行锁合称 next-key lock,间隙锁是开区间,每个 next-key lock 是前开后闭区间==。间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的。

Eg:如图

这里 session B 并不会被堵住。因为表 t 里并没有 c=7 这个记录,因此 session A 加的是间隙锁 (5,10)。而 session B 也是在这个间隙加的间隙锁。如果用 select * from t for update 要把整个表所有记录锁起来,就形成了 7 个 next-key lock,分别是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]。

注:间隙锁可能导致死锁:

3)解决幻读方式

a.间隙锁是在可重复读隔离级别下才会生效的。所以可重复读隔离级别+间隙锁b.读提交隔离级别加 binlog_format=row 的组合。

第二种方式解释一下:MySQL 通过 BINLOG 录入执行成功的 INSERT、UPDATE、DELETE 等更新数据的 SQL 语句,并由此实现 MySQL 数据库的恢复和主从复制。MySQL 的恢复机制(复制其实就是在 Slave Mysql 不断做基于 BINLOG 的恢复)有以下特点:

一是 MySQL 的恢复是 SQL 语句级的,也就是重新执行 BINLOG 中的 SQL 语句。二是 MySQL 的 Binlog 是按照事务提交的先后顺序记录的, 恢复也是按这个顺序进行的。

MySQL 的恢复机制要求:在一个事务未提交前,其他并发事务不能插入满足其锁定条件的任何记录,也就是不允许出现幻读。

2、脏读1)脏读(Dirty Read)

脏页:当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。也就是缓冲池中已经被修改,但是还没有刷新到磁盘。

脏数据:是指事务对缓冲池中行记录的修改,并且还没有被提交。

脏读指的就是在不同事务下,当前事务可以读到另外事务未提交的数据,简单来说就是可以读到脏数据。(脏读现象的发生条件是需要事务的隔离级别为READ UNCOMMITTED

2)刷脏页

内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。平时执行很快的更新操作,其实就是在写内存和日志,而 MySQL 偶尔“抖”一下的那个瞬间,可能就是在刷脏页(flush)。

(1)什么时候刷呢第一个场景对应的就是 InnoDB 的 redo log 写满了。这时候系统会停止所有更新操作,就需要将两个点之间的日志,对应的所有脏页都 flush 到磁盘上,把 checkpoint 往前推进,redo log 留出空间可以继续写。第二个场景对应的就是系统内存不足。当需要新的内存页,而内存不够用的时候,就要淘汰一些数据页,空出内存给别的数据页使用。如果淘汰的是“脏页”,就要先将脏页写到磁盘。第三个场景,对应的就是 MySQL 认为系统“空闲”的时候。第四个场景,对应的就是 MySQL 正常关闭的情况。这时候,MySQL 会把内存的脏页都 flush 到磁盘上,这样下次 MySQL 启动的时候,就可以直接从磁盘上读数据,启动速度会很快。(2)四种场景对性能的影响

第三种情况是属于 MySQL 空闲时的操作,这时系统没什么压力。而第四种场景是数据库本来就要关闭了。这两种情况下,你不会太关注“性能”问题。

第一种是“redo log 写满了,要 flush 脏页”,这种情况是 InnoDB 要尽量避免的。因为出现这种情况的时候,整个系统就不能再接受更新了,所有的更新都必须堵住。

第二种是“内存不够用了,要先将脏页写到磁盘”,这种情况其实是常态。InnoDB 用缓冲池(buffer pool)管理内存,缓冲池中的内存页有三种状态:第一种是,还没有使用的;第二种是,使用了并且是干净页;第三种是,使用了并且是脏页。InnoDB 的策略是尽量使用内存,因此对于一个长时间运行的库来说,未被使用的页面很少。而当要读入的数据页没有在内存的时候,就必须到缓冲池中申请一个数据页。这时候只能把最久不使用的数据页从内存中淘汰掉:如果要淘汰的是一个干净页,就直接释放出来复用;但如果是脏页呢,就必须将脏页先刷到磁盘,变成干净页后才能复用。

(3)InnoDB 刷脏页的控制策略

InnoDB 的刷盘速度就是要参考这两个因素:一个是脏页比例,一个是 redo log 写盘速度。==参数 innodb_max_dirty_pages_pct 是脏页比例上限,默认值是 75%==。InnoDB 会根据当前的脏页比例(假设为 M),算出一个范围在 0 到 100 之间的数字。InnoDB 每次写入的日志都有一个序号,当前写入的序号跟 checkpoint 对应的序号之间的差值,我们假设为 N。InnoDB 会根据这个 N 算出一个范围在 0 到 100 之间的数字,这个计算公式可以记为 F2(N)。根据上述算得的 F1(M) 和 F2(N) 两个值,取其中较大的值记为 R,之后引擎就可以按照 innodb_io_capacity 定义的能力乘以 R% 来控制刷脏页的速度。平时要多关注脏页比例,不要让它经常接近 75%。

脏页比例是通过Innodb_buffer_pool_pages_dirty/Innodb_buffer_pool_pages_total 得到的。

一旦一个查询请求需要在执行过程中先 flush 掉一个脏页时,这个查询就可能要比平时慢了。而 MySQL 中的一个机制,可能让你的查询会更慢:在准备刷一个脏页的时候,如果这个数据页旁边的数据页刚好是脏页,就会把这个“邻居”也带着一起刷掉;而且这个把“邻居”拖下水的逻辑还可以继续蔓延,也就是对于每个邻居数据页,如果跟它相邻的数据页也还是脏页的话,也会被放到一起刷

在 InnoDB 中,innodb_flush_neighbors 参数就是用来控制这个行为的,值为 1 的时候会有上述的“连坐”机制,值为 0 时表示不找邻居,自己刷自己的。在 MySQL 8.0 中,innodb_flush_neighbors 参数的默认值已经是 0 了,减少 SQL 语句响应时间。

eg:我这是低版本,设置0时情况:

四、加锁规则

这个规则说明:MySQL 后面的版本可能会改变加锁策略,所以这个规则只限于截止到现在的最新版本,即 5.x 系列 2、数据页的复用及空洞

如果相邻的两个数据页利用率都很小,系统就会把这两个页上的数据合到其中一个页上,另外一个数据页就被标记为可复用。如果我们用 delete 命令把整个表的数据删除呢?结果就是,所有的数据页都会被标记为可复用,但磁盘文件的大小是不会变的。也就是说,通过 delete 命令是不能回收表空间的。如果数据是按照索引递增顺序插入的,那么索引是紧凑的。但如果数据是随机插入的,就可能造成索引的数据页分裂。

更新索引上的值,可以理解为删除一个旧的值,再插入一个新值。不难理解,这也是会造成空洞的。也就是说,经过大量增删改的表,都是可能是存在空洞的。而重建表,就可以达到这样的目的。

3、重建表

这里,你可以使用 alter table A engine=InnoDB 命令来重建表。在 MySQL 5.5 版本之前,这个命令的执行流程跟我们前面描述的差不多,区别只是这个临时表 B 不需要你自己创建,MySQL 会自动完成转存数据、交换表名、删除旧表的操作。因此,在整个 DDL 过程中,表不能有更新,不然就会造成数据丢失。也就是说,这个 DDL 不是 Online 的。

MySQL 5.6 版本开始引入的 Online DDL,对这个操作流程做了优化。

重建表的流程:建立一个临时文件,扫描表 A 主键的所有数据页;用数据页中表 A 的记录生成 B+ 树,存储到临时文件中;生成临时文件的过程中,将所有对 A 的操作记录在一个日志文件(row log)中,对应的是图中 state2 的状态;临时文件生成后,将日志文件中的操作应用到临时文件,得到一个逻辑数据上与表 A 相同的数据文件,对应的就是图中 state3 的状态;用临时文件替换表 A 的数据文件。

由于日志文件记录和重放操作这个功能的存在,这个方案在重建表的过程中,允许对表 A 做增删改操作。这也就是 Online DDL。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3